Skip to content

Onboarded Model Experiments service on Stackit CLI - #1587

Open
haya-fatima wants to merge 14 commits into
stackitcloud:mainfrom
haya-fatima:feat/onboard-AI-MEx
Open

haya-fatima wants to merge 14 commits into
stackitcloud:mainfrom
haya-fatima:feat/onboard-AI-MEx

Conversation

@haya-fatima

@haya-fatima haya-fatima commented Sep 8, 2026 •

Copy link
Copy Markdown

Description

Onboarded the model experiments service to the STACKIT CLI and updated the relevant functionality. Added the necessary tests and verified the changes locally.

In case there is no issue present for your PR, please consider creating one.
At least please give us some description what you are trying to achieve and why your change is needed. -->

relates to #STACKITMLO-1718

Checklist

  • Issue was linked above
  • Code format was applied: make fmt
  • Examples were added / adjusted (see e.g. here)
  • Docs are up-to-date: make generate-docs (will be checked by CI)
  • Unit tests got implemented or updated
  • Unit tests are passing: make test (will be checked by CI)
  • No linter issues: make lint (will be checked by CI)

@haya-fatima
haya-fatima requested a review from a team as a code owner September 8, 2026 07:58
@haya-fatima
haya-fatima marked this pull request as draft September 8, 2026 08:16
@haya-fatima
haya-fatima marked this pull request as ready for review September 9, 2026 07:42
@github-actions

Copy link
Copy Markdown

This PR was marked as stale after 7 days of inactivity and will be closed after another 7 days of further inactivity. If this PR should be kept open, just add a comment, remove the stale label or push new commits to it.

@github-actions github-actions Bot added the Stale label Sep 19, 2026
@GokceGK GokceGK removed the Stale label Sep 28, 2026

func NewCmd(params *types.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "ai-model-experiments",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason why command is named as "ai-model-experiments"? I would prefer here we call it "model-experiments", as we onboarded terraform resources also like that. We can mention in descriptions, in short long fields as AI Model Experiment.

https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/modelexperiments_instance

I saw that in Portal it is called AI Model Experiment and we generally align with them, but then we will differ from SDK and Terraform Provider.

Comment thread go.mod
github.com/stackitcloud/stackit-sdk-go/services/iaas v1.14.5
github.com/stackitcloud/stackit-sdk-go/services/intake v0.11.2
github.com/stackitcloud/stackit-sdk-go/services/logs v0.10.2
github.com/stackitcloud/stackit-sdk-go/services/modelexperiments v0.3.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can onboard the newer version v0.3.1

)

func ConfigureClient(p *print.Printer, cliVersion string) (*modelexperiments.APIClient, error) {
return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.AIModelExperimentsCustomEndpointKey), false, genericclient.CreateApiClient[*modelexperiments.APIClient](modelexperiments.NewAPIClient))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.AIModelExperimentsCustomEndpointKey), false, genericclient.CreateApiClient[*modelexperiments.APIClient](modelexperiments.NewAPIClient))
return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.AIModelExperimentsCustomEndpointKey), false, modelexperiments.NewAPIClient)

viper.SetDefault(LogsCustomEndpointKey, "")
viper.SetDefault(CDNCustomEndpointKey, "")
viper.SetDefault(ValkeyCustomEndpointKey, "")
viper.SetDefault(VPNCustomEndpointKey, "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add it also here the endpoint key

viper.SetDefault(LogsCustomEndpointKey, "")
viper.SetDefault(CDNCustomEndpointKey, "")
viper.SetDefault(ValkeyCustomEndpointKey, "")
viper.SetDefault(VPNCustomEndpointKey, "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
viper.SetDefault(AIModelExperimentsCustomEndpointKey, "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If It is renamed to ModelExperiments, please adapt the suggestion

if resp == nil {
return "", ErrResponseNil
}
if resp.Instance.Name == "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can combine this check with the above one, like you have done in the GetTokenName function.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if resp.Instance.Name == "" {
if resp == nil || resp.Instance.Name == "" {
return "", ErrResponseNil
}
return resp.Instance.Name, nil


var (
testProjectID = uuid.NewString()
testRegion = "eu01"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a const and can be defined as

const (
testRegion = "eu01"
)

)

type mockSettings struct {
instanceFails bool

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getInstanceFails would be more understandable.

This is also valid for the following vars

getInstanceResp
getTokenFails
getTokenResp


func NewCmd(params *types.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("get %s", instanceIdArg),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as I already commented in the instance.go, command must be called "describe"

func NewCmd(params *types.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("get %s", instanceIdArg),
Short: "Gets an AI Model Experiments instance",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Short: "Gets an AI Model Experiments instance",
Short: "Shows details of an AI Model Experiments instance",

Args: args.SingleArg(instanceIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Get an AI Model Experiments instance with ID "xxx"`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`Get an AI Model Experiments instance with ID "xxx"`,
`Get details of an AI Model Experiments instance with ID "xxx"`,

examples.NewExample(
`Get an AI Model Experiments instance with ID "xxx"`,
`$ stackit ai-model-experiments instance get xxx`,
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity, Please add here one more example which is using the output flag

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
),
),
examples.NewExample(
`Get details of an AI Model Experiments instance with ID "xxx" in JSON format`,
`$ stackit ai-model-experiments instance get xxx --output-format json`),

return &model, nil
}

func buildGetInstanceRequest(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: function can be called buildRequest

model.Region,
model.InstanceId,
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a nitpick, but I think it will help a lot for the readability and uniformity across the cli. Please use the same indentations and single/multi line styling in return and function headers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *modelexperiments.APIClient) modelexperiments.ApiGetInstanceRequest {
return apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, model.Region, model.InstanceId)
}


return p.OutputResult(outputFormat, resp.Instance, func() error {
table := tables.NewTable()
table.SetHeader("ID", "NAME", "REGION", "STATUS")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the describe command output, we create table differently than the list command output. Please take a look at the other describe command output functions for example.

Besides that, in describe command we expect a more detailed output for the user. In the table view, you can also provide the same information shown in the json or yaml outputs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
table.SetHeader("ID", "NAME", "REGION", "STATUS")
func outputResult(p *print.Printer, outputFormat string, resp *modelexperiments.GetInstanceResponse) error {
if resp == nil {
return fmt.Errorf("response instance is nil")
}
return p.OutputResult(outputFormat, resp.Instance, func() error {
table := tables.NewTable()
table.AddRow("ID", resp.Instance.Id)
table.AddSeparator()
table.AddRow("NAME", resp.Instance.Name)
table.AddSeparator()
table.AddRow("REGION", utils.PtrString(resp.Instance.Region))
table.AddSeparator()
table.AddRow("STATUS", resp.Instance.State)
table.AddSeparator()
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
})
}

"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
)

var projectIdFlag = globalflags.ProjectIdFlag

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: you can use this directly as globalflags.ProjectIdFlag

}

var testProjectId = uuid.NewString()
var testInstanceId = uuid.NewString()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var testInstanceId = uuid.NewString()
var (
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
testClient = &modelexperiments.APIClient{DefaultAPI: modelexperiments.DefaultAPIServiceMock{}}
testProjectId = uuid.NewString()
testInstanceId = uuid.NewString()
)


var testProjectId = uuid.NewString()
var testInstanceId = uuid.NewString()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Region value has been used across the tests. Here you can define a const and use that one.

const testRegion = "eu01"


return flagValues
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you can define a fixtureArgValues function and use this in the following tests argValues fields. In case you need to make a change, it will be simpler.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testInstanceId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}

model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Region: "eu01",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as I mentioned above, if you define a const, you can use it in here

var testInstanceId = uuid.NewString()

func fixtureFlagValues(
mods ...func(*map[string]string),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you use pointer here *map[string]string?

flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add more unit tests here. You can see in other describe commands some example test cases you can add

@GokceGK GokceGK Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some examples

	{
		description: "no values",
		argValues:   []string{},
		flagValues:  map[string]string{},
		isValid:     false,
	},
	{
		description: "no arg values",
		argValues:   []string{},
		flagValues:  fixtureFlagValues(),
		isValid:     false,
	},
	{
		description: "no flag values",
		argValues:   fixtureArgValues(),
		flagValues:  map[string]string{},
		isValid:     false,
	},

}
}

func TestBuildGetInstanceRequest(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func TestBuildGetInstanceRequest(t *testing.T) {
func TestBuildGetInstanceRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest modelexperiments.ApiGetInstanceRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for following the similar coding style to the existing tests

func NewCmd(params *types.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists AI Model Experiments instances",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Short: "Lists AI Model Experiments instances",
Short: "Lists all AI Model Experiments instances",

Example: examples.Build(
examples.NewExample(
`List AI Model Experiments instances in a project`,
`$ stackit ai-model-experiments instance list --region eu01`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can give here multiple examples. One simple one without mentioning region flag. Region is a global flag, you dont really need to mention. But if you want to mention in your example, maybe give it in separate example and by mentioning .... in a project in region eu02 e.g

BEsides that one example with the output format would be nice. You can see examples in other list commands

return cmd
}

func parseInput(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the comments I left in other scripts about function headers are valid across whole PR. :)


modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In list commands, we use the limit flag to set the max number of entries to list. Please extend this. Examples can be found in other list commands


return p.OutputResult(outputFormat, resp.Instances, func() error {
if len(resp.Instances) == 0 {
p.Outputf("No instances found\n")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
p.Outputf("No instances found\n")
p.Outputf("No instances found for project %q\n", projectLabel)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think mentioning the project name would be a nicer output. You need to parse the projectLabel to outputResult function call.

}

table := tables.NewTable()
table.SetHeader("ID", "NAME", "REGION", "STATUS")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Portal shows in list view bucket name as "ARTIFACT STORAGE NAME", we can also add into here.

The dashboard link, might be skipped in my opinion.

table.AddRow(
instance.Id,
instance.Name,
*instance.Region,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*instance.Region,
utils.PtrString(instance.Region),

return &model, nil
}

func buildListInstancesRequest(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func buildListInstancesRequest(
func buildRequest(

"github.com/stackitcloud/stackit-cli/internal/pkg/testparams"
"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see comments from the describe (get) command, about the var grouping, const region, pointer usage etc.

}),
isValid: false,
},
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here tests can be extended

const (
nameFlag = "name"
descriptionFlag = "description"
labelFlag = "label"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can define multiple labels as key=value pairs and seperating with ,'s . So it would make sense to call this as labelsFlag = "labels"

`Create an instance with a description and labels`,
`$ stackit ai-model-experiments instance create --name my-tracking --description "team tracking server" --label env=prod`),
),
RunE: func(cmd *cobra.Command, args []string) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {

if err != nil {
return fmt.Errorf("create AI Model Experiments instance: %w", err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an async operation but wait handler using is missing here.

func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(nameFlag, "n", "", "Instance name")
cmd.Flags().String(descriptionFlag, "", "Instance description")
cmd.Flags().StringToString(labelFlag, nil, `Labels as key-value pairs, e.g. "--label env=prod"`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as I mentioned above, here we can provide multiple labels. We should adapt description also

GlobalFlagModel: globalFlags,
Name: flags.FlagToStringValue(p, cmd, nameFlag),
Description: flags.FlagToStringPointer(p, cmd, descriptionFlag),
Labels: labelsPtr,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check flags.FlagToStringToStringPointer function

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants